package com.nulldev.util.web.YouTubeExtractor.channel;

import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.CLIENT_VERSION;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_CL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_LABEL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_STS;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.UTC_OFFSET;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.VARIANTS_CHECKSUM;
import static com.nulldev.util.web.YouTubeExtractor.channel.ChannelExtractor.FORCE_DISABLE_NEW_CHANNEL_URLS;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getTabRendererWithContent;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTString;

import java.time.Duration;
import java.util.List;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.RandomUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.internal.backport.concurrency9.Maps;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.util.YTRequestObject;

public class ChannelPlaylistExtractor {

	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--debug", "-debug", "--trace", "--ChannelExtractor.debug"));

	public static class ChannelPlaylist {

		public final String playlistTitle;
		public final String url;
		public final String thumbnailUrl;

		public ChannelPlaylist(final String playlistTitle, final String url, final String thumbnailUrl) {
			this.playlistTitle = playlistTitle;
			this.url = url;
			this.thumbnailUrl = thumbnailUrl;
		}
	}

	public static class ChannelPlaylists {
		private final List<ChannelPlaylist> playlist;
		private final String continuation;

		public ChannelPlaylists(final List<ChannelPlaylist> playlist, final String continuation) {
			this.playlist = Variables.requireNonNullObject(playlist, "playlist");
			this.continuation = continuation;
		}

		public List<ChannelPlaylist> playlists() {
			return this.playlist;
		}

		public String continuation() {
			return this.continuation;
		}

		public boolean hasContinuation() {
			return !StringsUtil.isBlank(this.continuation);
		}
	}

	public static ChannelPlaylists extract(String channelID, final String continuation) throws Exception {
		return extract(channelID, continuation, true);
	}

	public static ChannelPlaylists extract(String channelID, final String continuation, final boolean sendConsentCookie) throws Exception {
		String URL = "https://www.youtube.com/channel/" + channelID + "/playlists?pbj=1";
		if (continuation != null && !continuation.isEmpty()) {
			URL = "https://www.youtube.com/youtubei/v1/browse?key=" + YouTubeExtractorConstants.API_KEY + "&prettyPrint=false";
		} else {
			if (channelID.startsWith("/user/")) {
				channelID = channelID.substring(1);
				URL = "https://www.youtube.com/" + channelID + "/playlists?pbj=1";
			} else if (channelID.startsWith("/") && !channelID.startsWith("/user/")) {
				URL = "https://www.youtube.com/user/" + channelID + "playlists?pbj=1";
			}
		}
		final boolean isJSONRequest = continuation != null && !continuation.isEmpty();
		String jpl = null;
		if (isJSONRequest) {
			if (continuation != null && !continuation.isEmpty()) {
				final UniJsonValue o = YTRequestObject.makeYTRequestObject(Maps.of());
				o.add("continuation", continuation);
				final String op = o.toString();
				jpl = op;
			}
		}
		String _result = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(15))
				.headers(sendConsentCookie ? YouTubeExtractor.DEFAULT_COOKIES : YouTubeExtractor.BLANK_COOKIES).header("X-YouTube-Client-Name", "1")
				.header("X-YouTube-Client-Version", CLIENT_VERSION).header("X-YouTube-Page-CL", PAGE_CL).header("X-YouTube-Page-Label", PAGE_LABEL)
				.header("X-YouTube-STS", PAGE_STS).header("X-YouTube-Utc-Offset", UTC_OFFSET).header("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM)
				.header(isJSONRequest ? new Entry("Content-Type", "application/json") : null).create(isJSONRequest ? HttpMethod.POST : HttpMethod.GET, jpl)
				.dataThenClose();
		if (_result.contains("\"apiUrl\":\"/youtubei/v1/browse\"") && !_result.contains("metadata") && !FORCE_DISABLE_NEW_CHANNEL_URLS) {
			log.warn("YouTube requests using the YouTubeI API, re-asking for data.");
			final UniJsonValue s = UniJsonValue.parse(_result);
			final String browseId;
			if (s.isArray()) {
				browseId = s.last().get("endpoint").get("browseEndpoint").getString("browseId");
			} else {
				browseId = s.get("endpoint").get("browseEndpoint").getString("browseId");
			}
			if (browseId == null) {
				log.error("Invalid browseId! Bailing out.");
				log.error(s);
				return null;
			} else
				log.debug("Using browseId: " + browseId);
			final UniJsonValue o = UniJsonValue.object();
			o.add("browseId", browseId);
			final UniJsonValue cbmp = UniJsonValue.object();
			cbmp.add("consentDay", 1);
			cbmp.add("consentHostnameOverride", "https://www.youtube.com");
			cbmp.add("urlOverride", "");
			o.add("consentBumpParams", cbmp);
			final UniJsonValue context = UniJsonValue.object();
			final UniJsonValue client = UniJsonValue.object();
			client.add("platform", "DESKTOP");
			client.add("userAgent", URLUtils.STANDARD_USERAGENT);
			client.add("utcOffsetMinutes", 60);
			client.add("hl", "en-GB");
			client.add("gl", "EN");
			client.add("clientName", "WEB");
			client.add("clientVersion", YouTubeExtractorConstants.CLIENT_VERSION);
			context.add("client", client);
			final UniJsonValue request = UniJsonValue.object();
			request.add("useSsl", true);
			request.add("internalExperimentFlags", UniJsonValue.array());
			request.add("consistencyTokenJars", UniJsonValue.array());
			context.add("request", request);
			final UniJsonValue user = UniJsonValue.object();
			user.add("lockedSafetyMode", false);
			context.add("user", user);
			context.add("clientScreenNonce", RandomUtil.hexString(24));
			o.add("context", context);
			final String op = o.toString();
			_result = HttpClient.getString().url("https://www.youtube.com/youtubei/v1/browse?key=" + YouTubeExtractorConstants.API_KEY + "&prettyPrint=false")
					.timeout(Duration.ofSeconds(15)).header("X-YouTube-Client-Name", "1").header("X-YouTube-Client-Version", CLIENT_VERSION)
					.header("X-YouTube-Page-CL", PAGE_CL).header("X-YouTube-Page-Label", PAGE_LABEL).header("X-YouTube-STS", PAGE_STS)
					.header("X-YouTube-Utc-Offset", UTC_OFFSET).header("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM).header("Referer", URL)
					.header("Content-Type", "application/json").create(HttpMethod.POST, op).dataThenClose();
		}
		final String result = _result;
		if (result.startsWith("[") || result.endsWith("]")) {
			final UniJsonValue object = UniJsonValue.parseArray(result);
			if (object == null || object.isNull()) {
				log.error("Failed to parse JSON payload!");
				log.debug("Payload: " + result);
				return null;
			}
			final UniJsonValue pbo = object.arrayGetContains("response");
			if (pbo.getSafe("response").contains("continuationContents")) {
				final variable<String> cont = new variable<String>(null);
				final UniJsonValue conts = pbo.get("response").get("continuationContents");
				throw new UnsupportedOperationException("TODO: Handle continuation JSON.");
			} else {
				final UniJsonValue _tabs = pbo.mget("response.contents.twoColumnBrowseResultsRenderer.tabs");
				Variables.requireNonNull(_tabs, "_tabs");
				final UniJsonIterableValue tabs = _tabs.asArray();
				final List<ChannelPlaylist> out = FastLists.list();
				final UniJsonValue playlistContent = getTabRendererWithContent(tabs);
				final variable<String> cont = new variable<String>(null);
				if (playlistContent != null) {
					UniJsonValue vids = playlistContent.get("tabRenderer").get("content").get("sectionListRenderer").get("contents");
					if (vids != null && !vids.isNull() && vids.isArray()) {
						if (vids.length() > 2 && vids.get(2).contains("itemSectionRenderer")) {
							vids = vids.get(2).get("itemSectionRenderer").get("contents").get(0);
						} else {
							vids = vids.get(0).get("itemSectionRenderer").get("contents").get(0);
						}
						// TODO: shelfRenderer here is an array with multiple items, handle those too :)
						if (vids.hasValue("gridRenderer")) {
							vids = vids.get("gridRenderer");
						} else if (vids.hasValue("shelfRenderer")) {
							vids = vids.get("shelfRenderer").get("content").get("horizontalListRenderer");
						}
						final UniJsonValue _c = vids.get("items");
						if (_c == null) {
							log.error("_c == null!");
							log.derror("tabs: " + tabs);
							return null;
						}
						_c.asArray().forEach((_video) -> {
							__for_each_grid_entry(_video, out, cont);
						});
						return new ChannelPlaylists(out, cont.get());
					} else {
						throw new UnsupportedOperationException("Unable to find playlists!");
					}
				} else {
					log.debug(pbo);
					throw new UnsupportedOperationException("Unable to find playlist object!");
				}
			}
		} else if (result.startsWith("{") || result.endsWith("}")) {
			final UniJsonValue object = UniJsonValue.parse(result);
			if (object == null || object.isNull()) {
				log.error("Failed to parse JSON payload!");
				log.debug("Payload: " + result);
				return null;
			}
			if (!object.contains("response")) {
				throw new UnsupportedOperationException("Unable to handle playlist object!");
			} else {
				final UniJsonValue response = object.get("response");
				final UniJsonValue _tabs = response.mget("contents.twoColumnBrowseResultsRenderer.tabs");
				Variables.requireNonNull(_tabs, "_tabs");
				final UniJsonIterableValue tabs = _tabs.asArray();
				final List<ChannelPlaylist> out = FastLists.list();
				final UniJsonValue playlistContent = getTabRendererWithContent(tabs);
				final variable<String> cont = new variable<String>(null);
				if (playlistContent != null) {
					UniJsonValue vids = playlistContent.get("tabRenderer").get("content").get("sectionListRenderer").get("contents");
					if (vids != null && !vids.isNull() && vids.isArray()) {
						if (vids.length() > 2 && vids.get(2).contains("itemSectionRenderer")) {
							vids = vids.get(2).get("itemSectionRenderer").get("contents").get(0);
						} else {
							vids = vids.get(0).get("itemSectionRenderer").get("contents").get(0);
						}
						// TODO: shelfRenderer here is an array with multiple items, handle those too :)
						if (vids.hasValue("gridRenderer")) {
							vids = vids.get("gridRenderer");
						} else if (vids.hasValue("shelfRenderer")) {
							vids = vids.get("shelfRenderer").get("content").get("horizontalListRenderer");
						}
						final UniJsonValue _c = vids.get("items");
						if (_c == null) {
							log.error("_c == null!");
							log.derror("tabs: " + tabs);
							return null;
						}
						_c.asArray().forEach((_video) -> {
							__for_each_grid_entry(_video, out, cont);
						});
						return new ChannelPlaylists(out, cont.get());
					} else {
						throw new UnsupportedOperationException("Unable to find playlists!");
					}
				} else {
					log.debug(result);
					throw new UnsupportedOperationException("Unable to find playlist object!");
				}
			}
		} else {
			log.debug("Response object: " + result);
			throw new UnsupportedOperationException("Unable to handle playlist object!");
		}
	}

	private static void __for_each_grid_entry(final UniJsonValue _video, final List<ChannelPlaylist> out, final variable<String> cont) {
		if (!_video.contains("gridPlaylistRenderer", "continuationItemRenderer")) {
			log.warn("Unknown playlist item: " + _video);
			return;
		} else if (_video.contains("gridPlaylistRenderer")) {
			final UniJsonValue gridPlaylistRenderer = _video.get("gridPlaylistRenderer");
			final String playlistTitle = getYTString(gridPlaylistRenderer.get("title"));
			final UniJsonValue navigationEndpoint = gridPlaylistRenderer.get("navigationEndpoint");
			final UniJsonValue watchEndpoint = navigationEndpoint.get("watchEndpoint");
			final String vid = watchEndpoint.getString("videoId");
			final String url = "/watch?v=" + vid + "&list=" + watchEndpoint.getString("playlistId");
			final String thumbnailUrl = gridPlaylistRenderer.get("thumbnail").get("thumbnails").last().getString("url");
			out.add(new ChannelPlaylist(playlistTitle, url, thumbnailUrl));
		} else if (_video.contains("continuationItemRenderer")) {
			cont.set(_video.get("continuationItemRenderer").get("continuationEndpoint").get("continuationCommand").getString("token"));
		}
	}

	public static void main(String[] args) throws Exception {
		Arguments.setup(args);
		final ChannelPlaylists cpl = extract("UCHFDIPcCMJ7IhNalPJGjKlA", null, true);
		JVM.println(cpl.continuation);
		ArrayUtils.printLln(cpl.playlist);
	}
}
